package com.j256.ormlite.hmos;

import com.j256.ormlite.db.DatabaseType;
import com.j256.ormlite.field.DataPersister;
import com.j256.ormlite.field.DataType;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.field.DatabaseFieldConfig;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.DatabaseTableConfig;
import com.j256.ormlite.utils.Constants;
import com.j256.ormlite.utils.LogUtil;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * HmosDatabaseConnection
 *
 */
public class DatabaseTableConfigUtil {
    /**
     * Set this system property to any value to disable the annotations hack which seems to cause problems on certain
     * operating systems.
     */
    public static final String DISABLE_ANNOTATION_HACK_SYSTEM_PROPERTY = "ormlite.annotation.hack.disable";
    private static final int COLUMN_NAME = 1;
    private static final int DATA_TYPE = 2;
    private static final int DEFAULT_VALUE = 3;
    private static final int WIDTH = 4;
    private static final int CAN_BE_NULL = 5;
    private static final int ID = 6;
    private static final int GENERATED_ID = 7;
    private static final int GENERATED_ID_SEQUENCE = 8;
    private static final int FOREIGN = 9;
    private static final int USE_GET_SET = 10;
    private static final int UNKNOWN_ENUM_NAME = 11;
    private static final int THROW_IF_NULL = 12;
    private static final int PERSISTED = 13;
    private static final int FORMAT = 14;
    private static final int UNIQUE = 15;
    private static final int UNIQUE_COMBO = 16;
    private static final int INDEX = 17;
    private static final int UNIQUE_INDEX = 18;
    private static final int INDEX_NAME = 19;
    private static final int UNIQUE_INDEX_NAME = 20;
    private static final int FOREIGN_AUTO_REFRESH = 21;
    private static final int MAX_FOREIGN_AUTO_REFRESH_LEVEL = 22;
    private static final int PERSISTER_CLASS = 23;
    private static final int ALLOW_GENERATED_ID_INSERT = 24;
    private static final int COLUMN_DEFINITON = 25;
    private static final int FOREIGN_AUTO_CREATE = 26;
    private static final int VERSION = 27;
    private static final int FOREIGN_COLUMN_NAME = 28;
    private static final int READ_ONLY = 29;
    private static final int FULL_COLUMN_DEFINITON = 30;

    private static Class<?> annotationFactoryClazz;
    private static Field elementsField;
    private static Class<?> annotationMemberClazz;
    private static Field nameField;
    private static Field valueField;
    private static int workedC = 0;

    private static final int[] CONFIG_FIELD_NUMS;

    static {
        /*
         * If we are dealing with older versions of the OS and if we've not disabled the annotation hack...
         */
        if (System.getProperty(DISABLE_ANNOTATION_HACK_SYSTEM_PROPERTY) == null) {
            CONFIG_FIELD_NUMS = lookupClasses();
        } else {
            CONFIG_FIELD_NUMS = null;
        }
    }

    /**
     * fromClass
     *
     * @param clazz Class
     * @param connectionSource ConnectionSource
     * @return the database config for the passed class, connectionsource
     * @throws SQLException Exception
     */
    public static <T> DatabaseTableConfig<T> fromClass(ConnectionSource connectionSource, Class<T> clazz)
            throws SQLException {
        DatabaseType databaseType = connectionSource.getDatabaseType();
        String tableName = DatabaseTableConfig.extractTableName(databaseType, clazz);
        List<DatabaseFieldConfig> fieldConfigs = new ArrayList<DatabaseFieldConfig>();
        for (Class<?> classWalk = clazz; classWalk != null; classWalk = classWalk.getSuperclass()) {
            for (Field field : classWalk.getDeclaredFields()) {
                DatabaseFieldConfig config = configFromField(databaseType, tableName, field);
                if (config != null && config.isPersisted()) {
                    fieldConfigs.add(config);
                }
            }
        }
        if (fieldConfigs.size() == 0) {
            return null;
        } else {
            return new DatabaseTableConfig<T>(clazz, tableName, fieldConfigs);
        }
    }

    /**
     * Return the number of fields configured using our reflection hack. This is for testing.
     *
     * @return worhedC Boolean
     */
    public static int getWorkedC() {
        return workedC;
    }

    /**
     * This does all of the class reflection fu to find our classes, find the order of field names, and construct our
     * array of ConfigField entries the correspond to the AnnotationMember array.
     * @return index 0 from int array.
     */
    private static int[] lookupClasses() {
        Class<?> annotationMemberArrayClazz;
        try {
            annotationFactoryClazz = Class.forName("org.apache.harmony.lang.annotation.AnnotationFactory");
            annotationMemberClazz = Class.forName("org.apache.harmony.lang.annotation.AnnotationMember");
            annotationMemberArrayClazz = Class.forName("[Lorg.apache.harmony.lang.annotation.AnnotationMember;");
        } catch (ClassNotFoundException e) {
            return new int[0];
        }

        Field fieldField;
        try {
            elementsField = annotationFactoryClazz.getDeclaredField("elements");
            elementsField.setAccessible(true);

            nameField = annotationMemberClazz.getDeclaredField("name");
            nameField.setAccessible(true);
            valueField = annotationMemberClazz.getDeclaredField("value");
            valueField.setAccessible(true);

            fieldField = DatabaseFieldSample.class.getDeclaredField("field");
        } catch (SecurityException e) {
            return new int[0];
        } catch (NoSuchFieldException e) {
            return new int[0];
        }

        DatabaseField databaseField = fieldField.getAnnotation(DatabaseField.class);
        InvocationHandler proxy = Proxy.getInvocationHandler(databaseField);
        if (proxy.getClass() != annotationFactoryClazz) {
            return null;
        }

        try {
            // this should be an array of AnnotationMember objects
            Object elements = elementsField.get(proxy);
            if (elements == null || elements.getClass() != annotationMemberArrayClazz) {
                return null;
            }

            Object[] elementArray = (Object[]) elements;
            int[] configNums = new int[elementArray.length];

            // build our array of field-numbers that match the AnnotationMember array
            for (int index = 0; index < elementArray.length; index++) {
                String name = (String) nameField.get(elementArray[index]);
                configNums[index] = configFieldNameToNum(name);
            }
            return configNums;
        } catch (IllegalAccessException e) {
            return null;
        }
    }

    /**
     * Convert the name of the @DatabaseField fields
     *
     * @param configName ConfigurationName
     * @return the name of fields
     */
    private static int configFieldNameToNum(String configName) {
        if (configName.equals(Constants.COL_NAME)) {
            return COLUMN_NAME;
        } else if (configName.equals(Constants.DATA_TYPE)) {
            return DATA_TYPE;
        } else if (configName.equals(Constants.DEFAULT_VAL)) {
            return DEFAULT_VALUE;
        } else if (configName.equals(Constants.WIDTH)) {
            return WIDTH;
        } else if (configName.equals(Constants.CAN_BE_NULL)) {
            return CAN_BE_NULL;
        } else if (configName.equals(Constants.ID)) {
            return ID;
        } else if (configName.equals(Constants.GENRATED_ID)) {
            return GENERATED_ID;
        } else if (configName.equals(Constants.GENERATED_SEQ)) {
            return GENERATED_ID_SEQUENCE;
        } else if (configName.equals(Constants.FORIEGN)) {
            return FOREIGN;
        } else if (configName.equals(Constants.USEGET_SET)) {
            return USE_GET_SET;
        } else if (configName.equals(Constants.UNKWNOWN_ENUM)) {
            return UNKNOWN_ENUM_NAME;
        } else if (configName.equals(Constants.THROW_IF_NULL)) {
            return THROW_IF_NULL;
        } else if (configName.equals(Constants.PERSISTED)) {
            return PERSISTED;
        } else if (configName.equals(Constants.FORMAT)) {
            return FORMAT;
        } else if (configName.equals(Constants.UNIQUE)) {
            return UNIQUE;
        } else if (configName.equals(Constants.UNIQUE_COMBO)) {
            return UNIQUE_COMBO;
        } else if (configName.equals(Constants.INDEX)) {
            return INDEX;
        } else if (configName.equals(Constants.UNIQUE_INDEX_DETAILS)) {
            return UNIQUE_INDEX;
        } else if (configName.equals(Constants.INDEX_NAME)) {
            return INDEX_NAME;
        } else if (configName.equals(Constants.UNIQUE_INDEX)) {
            return UNIQUE_INDEX_NAME;
        } else if (configName.equals(Constants.MAX_FOREIGN_AUTO_REFRESH)) {
            return FOREIGN_AUTO_REFRESH;
        } else if (configName.equals(Constants.MAX_FOREIGN_AUTO)) {
            return MAX_FOREIGN_AUTO_REFRESH_LEVEL;
        } else if (configName.equals(Constants.PERSISTANT)) {
            return PERSISTER_CLASS;
        } else if (configName.equals(Constants.MAGIC_FULLCOL_ALLOW)) {
            return ALLOW_GENERATED_ID_INSERT;
        } else if (configName.equals(Constants.MAGIC_FULLCOL_DEF)) {
            return COLUMN_DEFINITON;
        } else if (configName.equals(Constants.MAGIC_FULLCOL)) {
            return FULL_COLUMN_DEFINITON;
        } else if (configName.equals(Constants.MAGIC_AUTO)) {
            return FOREIGN_AUTO_CREATE;
        } else if (configName.equals(Constants.MAGIC_VERSION)) {
            return VERSION;
        } else if (configName.equals(Constants.MAGIC_COLUM_NAME)) {
            return FOREIGN_COLUMN_NAME;
        } else if (configName.equals(Constants.MAGIC_READ)) {
            return READ_ONLY;
        } else {
            throw new IllegalStateException("Could not find support for DatabaseField " + configName);
        }
    }

    /**
     * Extract{@link DatabaseField} annotation.
     *
     * @param databaseType DatabaseType
     * @param tableName TableName
     * @param field Field
     * @return config Boolean
     * @throws SQLException Exception
     */
    private static DatabaseFieldConfig configFromField(DatabaseType databaseType, String tableName, Field field)
            throws SQLException {
        if (CONFIG_FIELD_NUMS == null) {
            return DatabaseFieldConfig.fromField(databaseType, tableName, field);
        }
        DatabaseField databaseField = field.getAnnotation(DatabaseField.class);

        DatabaseFieldConfig config = null;
        try {
            if (databaseField != null) {
                config = buildConfig(databaseField, tableName, field);
            }
        } catch (Exception e) {
            LogUtil.info("Constant", "abcdef");
        }
        if (config == null) {
            return DatabaseFieldConfig.fromField(databaseType, tableName, field);
        } else {
            workedC++;
            return config;
        }
    }

    /**
     * Instead of calling the annotation methods directly, we peer inside the proxy and investigate the array of
     * AnnotationMember objects stored by the AnnotationFactory.
     *
     * @param field Field
     * @param databaseField DatabaseField
     * @param tableName TableName
     * @throws exception
     * @return config
     */
    private static DatabaseFieldConfig buildConfig(DatabaseField databaseField, String tableName, Field field)
            throws Exception {
        InvocationHandler proxy = Proxy.getInvocationHandler(databaseField);
        if (proxy.getClass() != annotationFactoryClazz) {
            return null;
        }
        Object elementsObject = elementsField.get(proxy);
        if (elementsObject == null) {
            return null;
        }
        DatabaseFieldConfig config = new DatabaseFieldConfig(field.getName());
        Object[] objs = (Object[]) elementsObject;
        for (int index = 0; index < CONFIG_FIELD_NUMS.length; index++) {
            Object value = valueField.get(objs[index]);
            if (value != null) {
                assignConfigField(CONFIG_FIELD_NUMS[index], config, field, value);
            }
        }
        return config;
    }

    /**
     * Converts from field/value from the {@link DatabaseField} annotation to {@link DatabaseFieldConfig} values. This
     * is very specific to this annotation.
     *
     * @param config Config
     * @param field Field
     * @param configNum ConfigNum
     * @param value Object
     */
    private static void assignConfigField(int configNum, DatabaseFieldConfig config, Field field, Object value) {
        switch (configNum) {
            case COLUMN_NAME:
                config.setColumnName(valueIfNotBlank((String) value));
                break;
            case DATA_TYPE:
                config.setDataType((DataType) value);
                break;
            case DEFAULT_VALUE:
                String defaultValue = (String) value;
                if (!(defaultValue == null || defaultValue.equals(DatabaseField.DEFAULT_STRING))) {
                    config.setDefaultValue(defaultValue);
                }
                break;
            case WIDTH:
                config.setWidth((Integer) value);
                break;
            case CAN_BE_NULL:
                config.setCanBeNull((Boolean) value);
                break;
            case ID:
                config.setId((Boolean) value);
                break;
            case GENERATED_ID:
                config.setGeneratedId((Boolean) value);
                break;
            case GENERATED_ID_SEQUENCE:
                config.setGeneratedIdSequence(valueIfNotBlank((String) value));
                break;
            case FOREIGN:
                config.setForeign((Boolean) value);
                break;
            case USE_GET_SET:
                config.setUseGetSet((Boolean) value);
                break;
            case UNKNOWN_ENUM_NAME:
                config.setUnknownEnumValue(DatabaseFieldConfig.findMatchingEnumVal(field, (String) value));
                break;
            case THROW_IF_NULL:
                config.setThrowIfNull((Boolean) value);
                break;
            case PERSISTED:
                config.setPersisted((Boolean) value);
                break;
            case FORMAT:
                config.setFormat(valueIfNotBlank((String) value));
                break;
            case UNIQUE:
                config.setUnique((Boolean) value);
                break;
            case UNIQUE_COMBO:
                config.setUniqueCombo((Boolean) value);
                break;
            case INDEX:
                config.setIndex((Boolean) value);
                break;
            case UNIQUE_INDEX:
                config.setUniqueIndex((Boolean) value);
                break;
            case INDEX_NAME:
                config.setIndexName(valueIfNotBlank((String) value));
                break;
            case UNIQUE_INDEX_NAME:
                config.setUniqueIndexName(valueIfNotBlank((String) value));
                break;
            case FOREIGN_AUTO_REFRESH:
                config.setForeignAutoRefresh((Boolean) value);
                break;
            case MAX_FOREIGN_AUTO_REFRESH_LEVEL:
                config.setMaxForeignAutoRefreshLevel((Integer) value);
                break;
            case PERSISTER_CLASS:
                @SuppressWarnings("unchecked")
                Class<? extends DataPersister> clazz = (Class<? extends DataPersister>) value;
                config.setPersisterClass(clazz);
                break;
            case ALLOW_GENERATED_ID_INSERT:
                config.setAllowGeneratedIdInsert((Boolean) value);
                break;
            case COLUMN_DEFINITON:
                config.setColumnDefinition(valueIfNotBlank((String) value));
                break;
            case FULL_COLUMN_DEFINITON:
                config.setFullColumnDefinition(valueIfNotBlank((String) value));
                break;
            case FOREIGN_AUTO_CREATE:
                config.setForeignAutoCreate((Boolean) value);
                break;
            case VERSION:
                config.setVersion((Boolean) value);
                break;
            case FOREIGN_COLUMN_NAME:
                config.setForeignColumnName(valueIfNotBlank((String) value));
                break;
            case READ_ONLY:
                config.setReadOnly((Boolean) value);
                break;
            default:
                throw new IllegalStateException("Could not find support for DatabaseField number " + configNum);
        }
    }

    /**
     * valueIfNotBlank
     *
     * @param value in string
     * @return value
     */
    private static String valueIfNotBlank(String value) {
        if (value == null || value.length() == 0) {
            return null;
        } else {
            return value;
        }
    }

    /**
     * DatabaseFieldSample
     *
     */
    private static class DatabaseFieldSample {
        @DatabaseField
        String field;
    }
}
